// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:meta/meta.dart';

import '../base/deferred_component.dart';
import '../base/logger.dart';
import '../convert.dart';
import '../dart/pub.dart';
import '../flutter_manifest.dart';
import '../project.dart';
import 'preview_manifest.dart';

/// Builds and manages the pubspec for the widget preview scaffold
class PreviewPubspecBuilder {
  const PreviewPubspecBuilder({
    required this.logger,
    required this.verbose,
    required this.offline,
    required this.rootProject,
    required this.previewManifest,
  });

  final Logger logger;
  final bool verbose;

  /// Set to true if pub should operate in offline mode.
  final bool offline;

  /// The Flutter project that contains widget previews.
  final FlutterProject rootProject;

  /// Details about the current state of the widget preview scaffold project.
  final PreviewManifest previewManifest;

  /// Adds dependencies on:
  ///   - dtd, which is used to connect to the Dart Tooling Daemon to establish communication
  ///     with other developer tools.
  ///   - flutter_lints, which is referenced by the analysis_options.yaml generated by the 'app'
  ///     template.
  ///   - google_fonts, which is used for the Roboto Mono font.
  ///   - stack_trace, which is used to generate terse stack traces for displaying errors thrown
  ///     by widgets being previewed.
  ///   - url_launcher, which is used to open a browser to the preview documentation.
  static const _kWidgetPreviewScaffoldDeps = <String>[
    'dtd',
    'flutter_lints',
    'google_fonts',
    'stack_trace',
    'url_launcher',
  ];

  /// Maps asset URIs to relative paths for the widget preview project to
  /// include.
  @visibleForTesting
  static Uri transformAssetUri(Uri uri) {
    // Assets provided by packages always start with 'packages' and do not
    // require their URIs to be updated.
    if (uri.path.startsWith('packages')) {
      return uri;
    }
    // Otherwise, the asset is contained within the root project and needs
    // to be referenced from the widget preview scaffold project's pubspec.
    return Uri(path: '../../${uri.path}');
  }

  @visibleForTesting
  static AssetsEntry transformAssetsEntry(AssetsEntry asset) {
    return AssetsEntry(
      uri: transformAssetUri(asset.uri),
      flavors: asset.flavors,
      transformers: asset.transformers,
    );
  }

  @visibleForTesting
  static DeferredComponent transformDeferredComponent(DeferredComponent component) {
    return DeferredComponent(
      name: component.name,
      // TODO(bkonyi): verify these library paths are always package: paths from the parent project.
      libraries: component.libraries,
      assets: component.assets.map(transformAssetsEntry).toList(),
    );
  }

  Future<void> populatePreviewPubspec({
    required FlutterProject rootProject,
    String? updatedPubspecPath,
  }) async {
    final FlutterProject widgetPreviewScaffoldProject = rootProject.widgetPreviewScaffoldProject;

    // Overwrite the pubspec for the preview scaffold project to include assets
    // from the root project. Dependencies are removed as part of this operation
    // and they need to be added back below.
    widgetPreviewScaffoldProject.replacePubspec(
      buildPubspec(
        rootProject: rootProject,
        widgetPreviewManifest: widgetPreviewScaffoldProject.manifest,
      ),
    );

    // Adds a path dependency on the parent project so previews can be
    // imported directly into the preview scaffold.
    const pubAdd = 'add';
    final workspacePackages = <String, String>{
      for (final FlutterProject project in <FlutterProject>[
        rootProject,
        ...rootProject.workspaceProjects,
      ])
        // Use `json.encode` to handle escapes correctly.
        project.manifest.appName: json.encode(<String, Object?>{
          // `pub add` interprets relative paths relative to the current directory.
          'path': widgetPreviewScaffoldProject.directory.fileSystem.path.relative(
            project.directory.path,
          ),
        }),
    };

    final PubOutputMode outputMode = verbose ? PubOutputMode.all : PubOutputMode.failuresOnly;
    await pub.interactively(
      <String>[
        pubAdd,
        if (offline) '--offline',
        '--directory',
        widgetPreviewScaffoldProject.directory.path,
        // Ensure the path using POSIX separators, otherwise the "path_not_posix" check will fail.
        for (final MapEntry<String, String>(:String key, :String value)
            in workspacePackages.entries)
          '$key:$value',
      ],
      context: PubContext.pubAdd,
      command: pubAdd,
      touchesPackageConfig: true,
      outputMode: outputMode,
    );

    // Adds dependencies required by the widget preview scaffolding.
    await pub.interactively(
      <String>[
        pubAdd,
        if (offline) '--offline',
        '--directory',
        widgetPreviewScaffoldProject.directory.path,
        ..._kWidgetPreviewScaffoldDeps,
      ],
      context: PubContext.pubAdd,
      command: pubAdd,
      touchesPackageConfig: true,
      outputMode: outputMode,
    );

    // Generate package_config.json.
    await pub.get(
      context: PubContext.create,
      project: widgetPreviewScaffoldProject,
      offline: offline,
      outputMode: outputMode,
    );

    previewManifest.updatePubspecHash(updatedPubspecPath: updatedPubspecPath);
  }

  void onPubspecChangeDetected(String path) {
    // TODO(bkonyi): trigger hot reload or restart?
    logger.printStatus('Changes to $path detected.');
    populatePreviewPubspec(rootProject: rootProject, updatedPubspecPath: path);
  }

  @visibleForTesting
  FlutterManifest buildPubspec({
    required FlutterProject rootProject,
    required FlutterManifest widgetPreviewManifest,
  }) {
    final deferredComponents = <DeferredComponent>[
      ...?rootProject.manifest.deferredComponents?.map(transformDeferredComponent),
      for (final FlutterProject project in rootProject.workspaceProjects)
        ...?project.manifest.deferredComponents?.map(transformDeferredComponent),
    ];

    // Copy the manifest with dependencies removed to handle situations where a package or
    // workspace name has changed. We'll re-add them later.
    return widgetPreviewManifest.copyWith(
      logger: logger,
      deferredComponents: deferredComponents,
      removeDependencies: true,
    );
  }
}
